﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace Noris.Tools.TraceVisualiser.Support
{
    /// <summary>
    /// Správce kroků Undo/Redo
    /// </summary>
    internal class UndoRedoCls
    {
        #region Konstrukce
        /// <summary>
        /// Konstruktor
        /// </summary>
        public UndoRedoCls(int maxCapacity)
        {
            this.MaxCapacity = (maxCapacity < 0 ? 0 : (maxCapacity > 250 ? 250 : maxCapacity));
            this.ItemList = new List<object>();
            this.ItemIndex = 0;
        }
        /// <summary>
        /// Maximální kapacita zásobníku
        /// </summary>
        public int MaxCapacity { get; protected set; }
        /// <summary>
        /// Seznam akcí. 
        /// Nové akce se vkládají na pozici this._ItemIndex. 
        /// Pokud se vkládají kamkoli doprostřed, pak se následující akce smažou (provedly se kroky zpět, a dělá se zcela nový krok jinam).
        /// Pokud se vkládají na konec, pak stávající zůstanou.
        /// Následně se aktualizuje index tak, aby ukazoval za poslední vloženou akci.
        /// </summary>
        protected List<object> ItemList { get; set; }
        /// <summary>
        /// Počet položek v poli ItemList.
        /// </summary>
        protected int ItemCount { get { return this.ItemList.Count; } }
		/// <summary>
		/// Index poslední položky (= ItemCount - 1)
		/// </summary>
		protected int ItemLast { get { return this.ItemCount - 1; } }
		/// <summary>
        /// Index. Při vkládání nových akcí ukazuje na posledně vloženou akci, při krokování Undo/Redo ukazuje na akci, kterou akce vrátila.
		/// Další Undo vrátí akci Index-1, a Redo vrátí akci Index+1.
        /// </summary>
        protected int ItemIndex { get; set; }
        #endregion
        #region Property, eventy, metody
        /// <summary>
        /// true, pokud lze vyzvednout a provést akci Undo (krok zpět)
        /// </summary>
        public bool CanUndo
        {
			get { return (this.ItemCount > 0 && this.ItemIndex > 0 && this.ItemIndex <= this.ItemLast); }
        }
        /// <summary>
        /// true, pokud lze vyzvednout a provést akci Redo (krok vpřed, opak Undo)
        /// </summary>
        public bool CanRedo
        {
			get { return (this.ItemCount > 0 && this.ItemIndex >= 0 && this.ItemIndex < this.ItemLast); }
        }
        /// <summary>
        /// Event, který je vyvolán po jakékoli změně v UndoRedo registru.
        /// Event je určen k aktualizaci vizuálního rozhraní (buttony Undo/Redo, podle hodnoty this.CanUndo a this.CanRedo
        /// </summary>
        public event EventHandler Change;
        /// <summary>
        /// Vyvolá event Change
        /// </summary>
        protected virtual void OnChange()
        {
            if (this.Change != null)
                this.Change(this, EventArgs.Empty);
        }
        /// <summary>
        /// Přidá nový prvek do seznamu, na aktuální pozici.
        /// Pokud aktuální pozice není na konci seznamu, zruší všechny prvky na aktuální pozici a za ní.
        /// Poté nastaví index za poslední prvek (za posledně přidaný).
        /// </summary>
        /// <param name="item"></param>
        public void AddNewItem(object item)
        {
            if (this.MaxCapacity == 0) return;
			if (this.SuppressedActivity) return;

			// V případě, že jsem dal několikrát Undo, a nyní vkládám novou položku, pak nebude možno dávat Redo = stávající položky Redo nemají význam:
            // Pokud index ukazuje na určitý prvek, za nímž existují další prvky,
			//  pak tyto následující prvky odeberu:
			int nxt = this.ItemIndex + 1;                 // Index první položky za aktuální pozicí, která se bude odebírat
			int cnt = this.ItemCount - nxt;               // Kolik položek je v seznamu za aktuální pozicí = položky k odebrání
            if (cnt > 0)
                this.ItemList.RemoveRange(nxt, cnt);

            // Ošetření maximální kapacity:
            if (this.ItemCount >= this.MaxCapacity)
                this.ItemList.RemoveRange(0, this.ItemCount - this.MaxCapacity + 1);

            // Přidám item na konec a nastavím index na poslední prvek (ten právě přidaný):
            this.ItemList.Add(item);
            this.ItemIndex = this.ItemLast;
            this.OnChange();
        }
        /// <summary>
        /// Metoda vyzvedne položku pro akci Undo (krok zpět).
        /// Pokud nelze provést Undo, vyhodí chybu.
        /// Vedle toho existuje metoda TryGetUndoItem(), která chybu nevyhazuje.
        /// Metoda posune ukazatel o jednu pozici doleva, umožní tak provádět sekvenčně Undo a Redo v rámci dostupných akcí.
        /// Metoda vyvolá event Change.
        /// </summary>
        /// <returns></returns>
        public object GetUndoItem()
        {
            object item;
            if (!this.TryGetUndoItem(out item))
                throw new InvalidOperationException("Undo stack is empty.");
            return item;
        }
        /// <summary>
        /// Metoda vyzvedne položku pro akci Undo (krok zpět).
        /// Pokud nelze provést Undo, vrátí false. Nevyhodí chybu.
        /// Vedle toho existuje metoda GetUndoItem(), která v případě neexistence položky vyhodí chybu..
        /// Metoda posune ukazatel o jednu pozici doleva, umožní tak provádět sekvenčně Undo a Redo v rámci dostupných akcí.
        /// Metoda vyvolá event Change.
        /// </summary>
        /// <returns></returns>
        public bool TryGetUndoItem(out object item)
        {
            item = null;
            if (!this.CanUndo)
                return false;

            item = this.ItemList[--this.ItemIndex];
            this.OnChange();
            return true;
        }
        /// <summary>
        /// Metoda vyzvedne položku pro akci Redo (krok vpřed).
        /// Pokud nelze provést Redo, vyhodí chybu.
        /// Vedle toho existuje metoda TryGetRedoItem(), která chybu nevyhazuje.
        /// Metoda posune ukazatel o jednu pozici doprava, umožní tak provádět sekvenčně Undo a Redo v rámci dostupných akcí.
        /// Metoda vyvolá event Change.
        /// </summary>
        /// <returns></returns>
        public object GetRedoItem()
        {
            object item;
            if (!this.TryGetRedoItem(out item))
                throw new InvalidOperationException("Redo stack is empty.");
            return item;
        }
        /// <summary>
        /// Metoda vyzvedne položku pro akci Redo (krok vpřed).
        /// Pokud nelze provést Redo, vrátí false. Nevyhodí chybu.
        /// Vedle toho existuje metoda GetRedoItem(), která v případě neexistence položky vyhodí chybu..
        /// Metoda posune ukazatel o jednu pozici doprava, umožní tak provádět sekvenčně Undo a Redo v rámci dostupných akcí.
        /// Metoda vyvolá event Change.
        /// </summary>
        /// <returns></returns>
        public bool TryGetRedoItem(out object item)
        {
            item = null;
            if (!this.CanRedo)
                return false;

            item = this.ItemList[++this.ItemIndex];
            this.OnChange();
            return true;
        }
        #endregion
		#region Pod pokličkou - možnost provádění změn bez vkládání do UndoRedo registru
		/// <summary>
		/// Metoda získá "pokličku", která po dobu své existence potlačí vstup nových akcí do UndoRedo registru.
		/// </summary>
		/// <returns></returns>
		public IDisposable GetHat()
		{
			return new Hat(this);
		}
		/// <summary>
		/// Příznak, že registr UndoRedo má potlačenou aktivitu vkládání nových akcí.
		/// Používá se to při aktivaci vlastních akcí ze zásobníku, které byly získány metodami GetUndo() atd.
		/// Jejich aktivace musí v aplikaci fyzicky proběhnout (například se změní časová osa), ale je třeba potlačit zápis do UndoRedo.
		/// </summary>
		public bool SuppressedActivity { get; protected set; }
		/// <summary>
		/// Poklička
		/// </summary>
		protected class Hat : IDisposable
		{
			internal Hat(UndoRedoCls undoRedo)
			{
				this.UndoRedo = undoRedo;
				this.UndoRedo.SuppressedActivity = true;
			}
			internal UndoRedoCls UndoRedo;
			void IDisposable.Dispose()
			{
				this.UndoRedo.SuppressedActivity = false;
			}
		}
		#endregion
        #region Static služby - detekce klávesy a odpovídající akce
        /// <summary>
        /// Metoda detekuje stisknutou klávesu, zda odpovídá určité akci při dodržení daných konvencí.
        /// Vrací odpovídající akci.
        /// </summary>
        /// <param name="e">Data o klávese, typicky v handleru KeyDown</param>
        /// <param name="backSpaceUndo">Detekovat BackSpace = Undo?</param>
        /// <param name="ctrlCursor">Detekovat Ctrl + kursor doleva/doprava = Undo/Redo?</param>
        /// <param name="ctrlZY">Detekovat Ctrl + Z/Y = Undo/Redo?</param>
        /// <returns></returns>
        internal static UndoRedoActionType DetectKeyEvent(System.Windows.Forms.KeyEventArgs e, bool backSpaceUndo, bool ctrlCursor, bool ctrlZY)
        {
            if (backSpaceUndo && e.Modifiers == Keys.None && e.KeyValue == 8)    // Samotný BackSpace
                return UndoRedoActionType.Undo;
            if (ctrlCursor && e.Modifiers == Keys.Control && e.KeyValue == 37)   // Levý Ctrl + kurzor doleva         = Undo
                return UndoRedoActionType.Undo;
            if (ctrlCursor && e.Modifiers == Keys.Control && e.KeyValue == 39)   // Levý Ctrl + kurzor doprava        = Redo
                return UndoRedoActionType.Redo;
            if (ctrlZY && e.Modifiers == Keys.Control && e.KeyValue == 90)       // Levý Ctrl + Z (nahoře mezi T a U) = Undo
                return UndoRedoActionType.Undo;
            if (ctrlZY && e.Modifiers == Keys.Control && e.KeyValue == 89)       // Levý Ctrl + Y (vlevo dole pod A)  = Redo
                return UndoRedoActionType.Redo;
            return UndoRedoActionType.None;
        }
        #endregion
    }
	/// <summary>
	/// Typ akce
	/// </summary>
	public enum UndoRedoActionType
	{
		None,
		/// <summary>
		/// Akce typu Undo
		/// </summary>
		Undo,
		/// <summary>
		/// Akce typu Redo
		/// </summary>
		Redo
	}
}
